﻿using System;
using System.Collections.Generic;
using System.Peppers.UI;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.Win32;
using System.ComponentModel;

namespace System.Peppers.Win32
{
    /// <summary>
    /// Class containing information about screen resolution, etc.
    /// *NOTE: extended version of System.Windows.Forms.Screen
    /// </summary>
    public sealed class Screen : UnmanagedObject
    {
        #region Static
        private const int MONITORINFOF_PRIMARY = 1;
        private static readonly bool _multiMonitorSupport = (User32.GetSystemMetrics(SystemMetric.SM_CMONITORS) != 0);
        private static readonly IntPtr PRIMARY_MONITOR = (IntPtr)(-1163005939);
        private static Screen[] _screens;
        private static HandleRef HANDREF_EMPTY = new HandleRef();

        private static Rectangle GetPrimaryVirtualScreen()
        {
            if (_multiMonitorSupport)
            {
                return new Rectangle(
                    User32.GetSystemMetrics(SystemMetric.SM_XVIRTUALSCREEN),
                    User32.GetSystemMetrics(SystemMetric.SM_YVIRTUALSCREEN),
                    User32.GetSystemMetrics(SystemMetric.SM_CXVIRTUALSCREEN),
                    User32.GetSystemMetrics(SystemMetric.SM_CYVIRTUALSCREEN));
            }
            return new Rectangle(Point.Empty, GetPrimaryMonitorSize());
        }

        private static Size GetPrimaryMonitorSize()
        {
            return new Size(
                User32.GetSystemMetrics(SystemMetric.SM_CXSCREEN),
                User32.GetSystemMetrics(SystemMetric.SM_CYSCREEN));
        }

        private static Rectangle GetPrimaryWorkingArea()
        {
            RECT rc = new RECT();
            User32.SystemParametersInfo(SPI.SPI_GETWORKAREA, 0, ref rc, 0);
            return Rectangle.FromLTRB(rc.left, rc.top, rc.right, rc.bottom);
        }

        /// <summary>
        /// Returns the first monitor marked as primary
        /// </summary>
        public static Screen PrimaryScreen
        {
            get
            {
                if (!_multiMonitorSupport)
                {
                    return new Screen(PRIMARY_MONITOR);
                }
                Screen[] allScreens = AllScreens;
                for (int i = 0; i < allScreens.Length; i++)
                {
                    if (allScreens[i].Primary)
                    {
                        return allScreens[i];
                    }
                }
                return null;
            }
        }

        /// <summary>
        /// Returns an array of screens
        /// </summary>
        public static Screen[] AllScreens
        {
            get
            {
                if (_screens == null)
                {
                    if (_multiMonitorSupport)
                    {
                        MonitorEnumCallback callback = new MonitorEnumCallback();
                        User32.MonitorEnumProc lpfnEnum = new User32.MonitorEnumProc(callback.Callback);
                        IntPtr dC = User32.GetDC(HANDREF_EMPTY);
                        try
                        {
                            User32.EnumDisplayMonitors(new HandleRef(null, dC), null, lpfnEnum, IntPtr.Zero);
                        }
                        finally
                        {
                            User32.ReleaseDC(HANDREF_EMPTY, new HandleRef(null, dC));
                        }
                        if (callback.Screens.Count > 0)
                        {
                            _screens = callback.Screens.ToArray(); ;
                        }
                        else
                        {
                            _screens = new Screen[] { new Screen(PRIMARY_MONITOR) };
                        }
                    }
                    else
                    {
                        _screens = new Screen[] { PrimaryScreen };
                    }
                    SystemEvents.DisplaySettingsChanged += new EventHandler(Screen.OnDisplaySettingsChanged);
                }
                return _screens;
            }
        }
        #endregion

        private Rectangle _bounds;
        private Rectangle _workingArea;

        internal Screen(IntPtr monitor)
            : this(monitor, IntPtr.Zero)
        { }

        internal Screen(IntPtr monitor, IntPtr hdc)
        {
            Refresh(monitor, hdc);
        }

        private void Refresh(IntPtr monitor, IntPtr hdc)
        {
            IntPtr handle = hdc;
            if (!_multiMonitorSupport || monitor == PRIMARY_MONITOR)
            {
                _bounds = GetPrimaryVirtualScreen();
                _workingArea = GetPrimaryWorkingArea();
                Primary = true;
                DeviceName = null;
            }
            else
            {
                MONITORINFOEX info = new MONITORINFOEX();
                User32.GetMonitorInfo(new HandleRef(null, monitor), info);
                _bounds = Rectangle.FromLTRB(info.rcMonitor.left, info.rcMonitor.top, info.rcMonitor.right, info.rcMonitor.bottom);
                _workingArea = Rectangle.FromLTRB(info.rcWork.left, info.rcWork.top, info.rcWork.right, info.rcWork.bottom);
                Primary = (info.dwFlags & MONITORINFOF_PRIMARY) != 0;
                for (int i = info.szDevice.Length; (i > 0) && (info.szDevice[i - 1] == '\0'); i--) ;
                DeviceName = new string(info.szDevice);
                DeviceName = DeviceName.TrimEnd(new char[1]);
                if (hdc == IntPtr.Zero)
                {
                    handle = Gdi32.CreateDC(DeviceName, null, null, new HandleRef(null, hdc));
                }
            }
            Handle = monitor;
            BitDepth = Gdi32.GetDeviceCaps(new HandleRef(null, handle), DeviceCap.BITSPIXEL) *
                Gdi32.GetDeviceCaps(new HandleRef(null, handle), DeviceCap.PLANES);
            if (hdc != handle)
            {
                Gdi32.DeleteDC(new HandleRef(null, handle));
            }
        }

        /// <summary>
        /// Checks equality by handle
        /// </summary>
        /// <param name="obj">Screen to compare</param>
        /// <returns>True if equal</returns>
        public override bool Equals(object obj)
        {
            if (obj is Screen)
            {
                Screen screen = (Screen)obj;
                if (Handle == screen.Handle)
                {
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// Returns handle for hash code
        /// </summary>
        /// <returns>Hash code</returns>
        public override int GetHashCode()
        {
            return (int)Handle;
        }

        /// <summary>
        /// Bit depth of screen
        /// </summary>
        public int BitDepth
        {
            get;
            private set;
        }

        /// <summary>
        /// Bounds of screen
        /// </summary>
        public Rectangle Bounds
        {
            get
            {
                return _bounds;
            }
        }

        /// <summary>
        /// Device name of screen
        /// *NOTE: will be null if system only has 1 monitor
        /// </summary>
        public string DeviceName
        {
            get;
            private set;
        }

        /// <summary>
        /// Returns true if primary monitor
        /// </summary>
        public bool Primary
        {
            get;
            private set;
        }

        /// <summary>
        /// Returns the working area of the screen
        /// </summary>
        public Rectangle WorkingArea
        {
            get
            {
                return _workingArea;
            }
        }

        /// <summary>
        /// Sets a new screen resolution for the monitor
        /// *NOTE: throws Win32Exception on error
        /// </summary>
        /// <param name="bounds">New bounds of screen</param>
        public void SetBounds(Rectangle bounds)
        {
            if (Bounds != bounds)
            {
                DEVMODE devmode = new DEVMODE();

                while (User32.EnumDisplaySettings(null, ENUM.ENUM_CURRENT_SETTINGS, ref devmode) != 0)
                {
                    devmode.ToString();
                }

                if (User32.EnumDisplaySettings(null, ENUM.ENUM_CURRENT_SETTINGS, ref devmode) != 0)
                {
                    devmode.dmPositionX = bounds.X;
                    devmode.dmPositionY = bounds.Y;
                    devmode.dmPelsWidth = bounds.Width;
                    devmode.dmPelsWidth = bounds.Height;

                    DISP_CHANGE result = User32.ChangeDisplaySettings(ref devmode, CDS.CDS_TEST);
                    if (result < DISP_CHANGE.DISP_CHANGE_SUCCESSFUL)
                    {
                        throw new Win32Exception((int)result, result.ToString());
                    }
                    else
                    {
                        result = User32.ChangeDisplaySettings(ref devmode, CDS.CDS_UPDATEREGISTRY);
                        if (result < DISP_CHANGE.DISP_CHANGE_SUCCESSFUL)
                        {
                            throw new Win32Exception((int)result, result.ToString());
                        }
                        else
                        {
                            Refresh(Handle, IntPtr.Zero);
                        }
                    }
                }
                else
                {
                    throw new Win32Exception();
                }
            }
        }

        #region Callback
        private static void OnDisplaySettingsChanged(object sender, EventArgs e)
        {
            SystemEvents.DisplaySettingsChanged -= new EventHandler(Screen.OnDisplaySettingsChanged);
            _screens = null;
        }

        private sealed class MonitorEnumCallback
        {
            public MonitorEnumCallback()
            {
                Screens = new List<Screen>();
            }

            public List<Screen> Screens
            {
                get;
                private set;
            }

            public bool Callback(IntPtr monitor, IntPtr hdc, IntPtr lprcMonitor, IntPtr lparam)
            {
                Screens.Add(new Screen(monitor, hdc));
                return true;
            }
        }
        #endregion
    }
}
